1.分布式系统无法保障绝对可用,相信大家都碰到过软件系统长时间不可用。面对类似问题,美国经济学家⽶歇尔·渥克提出了灰犀牛理论,用灰犀牛⽐喻⼤概率且影响巨⼤的潜在危机。2.如果你也面临复杂系统稳定性保障的难题,推荐阅读本文,武老师给你讲述B站如何遭遇、盯紧、应对稳定性”灰犀牛“的故事,希望对你有一定启发。
作者介绍
B站在线SRE负责人-武安闯「TakinTalks」稳定性技术交流平台特聘讲师,2016年加入B站,深度参与B站微服务拆分、云原生改造、高可用建设、SRE转型和稳定性 体系落地等项目 ,如Oncall、问题管理&复盘、高可用建设、多活容灾、混沌工程、SLO运营、容量管理等 。当前关注B站SRE的稳定性体系建设和推广,对SRE的实践有深入的探索与思考。
温馨提醒:本文共计7166字,预计花费10分钟阅读
上周B站技术发的“713事故”复盘文章爆了,很多小伙伴都在关注我们B站的高可用建设,其实一直以来我们在这个方向做了很多的努力和尝试。看了文章的小伙伴都知道事故是由于SLB层面故障引起的,我们最终是通过多活进行服务恢复的,当然这其中也存在很多其他的问题,今天我想就以下几个方面来跟大家分享我们是怎么从故障中汲取经验并进行优化的。
故障的分析和复盘是我们SRE永远绕不开的工作,对于SRE来说,想要定位问题必须要对系统架构和业务架构有足够的了解,这是一切工作开展的基础。我将分享B站的高可用方案相关建设,为了便于大家理解,这边先给大家介绍一下B站的系统架构。
1、先来看看整体系统架构
B站的架构主要包含用户访问、接入层、服务层、中间件/平台以及基础设施。
用户访问层:用户访问是多端的,有APP、Web、多屏,包括OTT电视等;
接入层:主要有三部分,我们的动态DCDN、第三方的商业CDN以及七层的SLB负载均衡,SLB下面还有一层API GW;
服务层:第一层是服务的BFF网关也是服务的Interface层,网关层下面是我们服务的Service层;
中间件/平台:比如核心的中间件缓存、DB、可观测系统,还有我们的KV和对象存储、CMDB和业务流程平台等;
基础设施:包含Paas、Iaas、混合云等。
2、每一层架构做了哪些高可用方案?
每一层架构都会有不同类型的常见故障,针对不同层面的问题和架构有不同的高可用方案,下面我会针对接入层、服务层、中间件这3个层级的高可用方案展开说一说。前面有提到我们接入层有DCDN、SLB和API GW,那在实际的架构里面,用户会基于DNS和HTTP DNS来访问我们的DCDN的节点,然后DCDN回源时会由边缘的POP点来做流量的汇聚并进入我们多个机房,因为我们做了多活的架构,所以是多个可用区的模式。在这里面可能出现的故障有网络故障、组件故障、服务故障以及机房故障常见4种,比如说用户访问DCDN节点时运营商网络故障或者是CDN节点回到机房的骨干网故障都属于网络故障,而我们“713”故障就是组件层面的SLB的故障;服务层的故障就比较多了,服务的代码bug、性能过载都会导致故障,机房故障就不太常见了。针对以上这些常见的故障,目前常见的高可用方案大概有这些:- 在DNS故障的时候,很多公司都会想到降级到HTTP DNS;
- 对于端上来说APP请求解析到一个地区返回多个DCDN节点,让移动端来做一个最佳的选路;
- 当我们APP在访问DCDN时出现网络层面的故障,我们可以降低到第三方的CDN,对我们自己的DCDN做容灾;
- 当CDN回源的时候会走我们的pop点进行流量汇聚,我们的pop点有多个可以做线路的互备;
因为我们“713故障”是SLB层的问题,所以在针对这个部分的高可用方案会做的多一些。
- SLB向后段转发的时候是可以发现多个可用区里的服务的,包含API GW以及没有走API GW的其他服务;
- 当单可用区的节点故障了,是可以自动降级到其他可用区的节点的;
- 当SLB所代理的服务出现异常的时候,我们也可以做对应的API的降级、熔断和限流等;
- 对SLB故障的处理流程做了优化,之前新建一套集群包括配置公网IP到检验完成需要花一个小时的时间,现在我们可以做到5分钟重建一套集群并完成配置与下发。
- API GW层面我们把很多SLB的能力给下放了,API GW也可以返现多可用区的服务节点,当出现故障时也能自动降级到其他可用区,同时它也支持API的降级、熔断和限流等;
SLB是南北向的流量架构,但服务层的流量其实是东西向的流量,服务层的高可用方案可能更偏向于微服务侧的治理。对于SRE来讲,是必须要知道自己所负责业务系统的治理能力的,不然在业务系统出现故障的时候,你就只能干等着了,不能给业务一个合理的止损建议和决策。接下来就详细说说服务东西向流量的高可用应该怎么做。我们的服务发现是通过Discovery发现组件来实现的,采用的是多可用区的部署,比如Service B在可用区A和可用区B都有部署,部署是k8s多pod模式,分布在不同的物理机上。由于采购的机器批次不一样,机器有新有老就会导致算力不一致。如果流量过载怎么办?两个可用区其中一个可用区故障了又该怎么办呢?我们这里有一些常见的故障预案跟大家分享一下。针对算力不一致的问题,我们内部在做微服务调用时有一个P2C的算法,它的核心逻辑是在服务间调用的时候,服务端会返回自己的目前的cpu使用率,客户端可以分析服务的响应RT、等待的response、成功率等来选择最优的server节点来进行动态的权重调整。这就能保证即使不同节点的算力不一样,但最在终实例层面或者容器层面的CPU使用率是均衡的。我们服务也是有熔断的策略,熔断比较常见这边就不展开叙述了。由于我们的服务是部署多可用区的,当一个可用区的服务故障时我们会通过服务发现来调用其他可用区的服务,如果这个服务所有可用区都故障了,我们就会采用内容质量降级的策略,具体分为服务降级、内容降级2种具体场景。服务降级就是本来请求服务B的,不请求服务B转而请求服务C;内容降级就是将数据在本地缓存下来,当服务不可用了就从本地缓存来响应服务。比如我们B站的首页推荐功能,当推荐服务不可用的时候,我们会降级到一些热门的稿件让用户有视频可以看,虽然这个视频不是用户真正喜欢看的,但至少保证用户有视频可看不会出现白屏或者空窗的情况。在服务间调用的时候遇到这种流量过载一般会采取限流,B站微服务间限流有两种模式,一是全局流控,是分布式的全局限流,通过微服务的框架拦截层面来实现的。它的限流对象是东西向的流量调用,比如说HTTP、grpc、mysql数据库的限流。另一种是单节点的动态流控,叫BBR的限流,它是基于Google BBR拥塞控制算法来实现的,它会基于我们服务单节点的cpu使用率、成功率、RT去动态决定这个节点现在的请求要不要处理,进行自适应的限流保护。
除了服务间的调用,服务对中间件的高可用也特别重要,因为服务间调用和中间件依赖故障,基本上就是服务中最高频率的两种故障了。图上可以看到我们核心服务的中间件架构,我们的服务经常会用到缓存来进行数据加速、读cachemiss、读DB,写的场景一般是通过MQ来做异步持久化,然后会有专门的Job去消费。在更新缓存的时候我们会有另一个场景,数据库做了主从同步,有一个canal的组件会来消费我们从库的binlog,再把消费过来的数据写到MQ里面,然后再通过Job来刷新我们的缓存。从这个架构里可以看到我们用到的核心中间件有3个,一个是缓存,我们内部几乎所有的服务都会用到,还有消息队列和DB。在这套架构里面我们的数据是保证最终一致的,因为B站的场景很多数据对用户来讲并不需要强一致,比如说弹幕、评论、收藏这种数据,只要做到最终一致就可以了,当然电商和支付的场景就不适合这种架构了。对于缓存来讲,我们目前支持Redis Cluster、Redis、Memcache3种模式,但在我们生产的实际使用里边,我们是建议先用Redis Ciuster,然后再用Redis的单节点,最后再用Memcache。因为Memcache是不支持集群架构的,同时它也没有数据持久化的模式,日常中使用中也没有办法进行数据迁移。B站在2017-2018年的时候,很多业务是直连Redis Cluster的,用的不同的SDK,有java、Php、C++等。每一种语言当时都出过bug,每个部门使用Redis Cluster的业务基本都炸过一次。Redis在3.0和4.0版本的时候是一个单线程的服务模型,升级到6.0之后正式引入了I/O Thread多线程,在之前单线程的模型下用短连接的形式去请求Redis,它的性能下降会特别严重。我们在物理机上做过测试,单核的场景,长连接能跑到十几万的QPS,如果是短连接的话会下降近10倍,只有一两万的QPS。当时我们有个拜年纪的活动,2016、2017年参加过的同学应该知道,它是一个点播的活动,活动一开始用户会给视频充电,这个充电的服务是一个java的服务用到的是Jedis SDK,SDK的连接池有maxtotal 和maxidle两个配置,当连接数量超过maxidle的值以后,后续的连接就变成短连接了,导致活动一开始用户大量充电,服务就挂了。从那之后我们就开始建设proxy代理,通过sidecar模式来部署,近两年我们的Redis Cluster就再也没有出现过那种大规模的故障了。因为sidrcar模式部署需要容器,为了降本增效我们今年又新增了proxyless sdk的部署模式。MQ主要是做数据的临时持久化和异步写DB的,它是一个集中式代理部署的模式,提供redis以及gRPC协议的访问,当时我们的代理是使用go语言的 kafka的sarama sdk来实现的,也是各种炸。2020年我们B站经常做一些运营活动所以用户的流量增长的很快,MQ层面的连接数、topic增长的都很快,代理方面压力也很大,动不动就故障。后来我们微服务团队优化了sarama的机制,核心服务也做了降级能力,可以绕过MQ通过RPC方式跟job直接通信,实现容灾。数据库mysql这一层是由我们专门的数据库DBA负责的,现在也是用一个proxy 代理、 sidecar模式去部署的。这种模式上线之后实现了读写分离,对业务侧是透明的。我们最常用的就是对异常SQL的拦截能力,比如说管理后台有运营同学查了一个数据触发了mysql或者未压测上线的慢sql,然后导致数据库过载,影响到我们C端的服务的时候,可以通过SQL proxy来把这个SQL做一个拦截,及时恢复我们的数据库,让C端用户可以正常使用。同时通过 proxy联动内部的BRM的高可用组件,当我们数据库或者某个节点不可用了,它可以自动感知并切换。
B站有了上面这么多高可用能力,我们的业务还会不会炸呢?
在事故的过程中我们也发现了许多的问题,从结果来看可以知道我们服务快速止损靠的就是多活,所以说多活是我们机房级别故障时容灾止损最快的一个方案了。在这次故障之后呢,我们详细复盘了我们的多活架构,发现里面存在一些问题:- 多活元信息是没有平台管理的。我们哪个业务做了多活业务、是什么类型的多活、是同城的还是异地单元化、哪些业务是哪些url的规则支持多活的、当前多活的流量调度策略是什么、用户是随机回到我们多个机房的,还是要基于用户的ip或者用户的一些设备ID来做路由的策略……这些没有地方维护,我们当时只能用文档来维护,这导致信息割裂的特别严重,信息的更新也不及时。
- 多活切量能力是完全依赖我们CDN运维。因为我们B站的多活流量调度是在边缘的DCDN侧来实现的,常见的流程是,SRE提出一个切量的需求来告诉CDN的同学,同时告诉他们要切哪个域名、哪一个URL规则,然后CDN发起变更开始切量,同时同步到SRE和研发,让SRE和研发同学一起来观察服务的状态和多机房的流量。
假如在过程中漏了一个接口,或者漏了一条规则,那整个流程要再走一遍,导致我们当时的多活切量效率极差,并且容灾的效果也不好,基于这些背景我们定了优化两个方向:一是多活的基础能力建设,二是我们多活的管控能力提升。
1、梳理相关元信息
我们哪些业务做了多活?应用的是什么多活模式?在多活里每个机房的定位是什么?这些相关的元信息我们是没有的,所以我们首先做的事情就是梳理相关的元信息。
前面有提到我们哪些业务做了多活,这些信息我们是没有的,所以一开始就着手于业务模型的梳理与定义。我们之前的业务模型应用的是三级结构,是一级是部门,二级是项目,三级是应用。项目这个级别有时代表服务的组织,有时代表服务的业务,信息比较混乱导致不能反映出业务真实的组织和相关信息。后来我们对模型进行了拆分,应用以业务维度做聚合,业务再向上关联的时候,分为两个视角,一个视角是组织,这个组织就是你真实的组织架构,用于我们的权限、审批、预算和成本相关的场景。另一个视角是业务所属的业务域。对业务来讲它是一个多活架构的管理单元,也是多活的一个治理单元。比如说我们要推多活覆盖的时候就会按业务维度进行。CRG的概念是蚂蚁多活的概念,CRG代表了Gzone、Rzone、Czone三种模式,B站引入了这个概念。- Gzone的模式下用户间的数据都是共享的,B站的视频播放、番剧播放、稿件信息、直播间,都偏向于平台侧的数据,这种业务场景,都可以做成Gzone的模式。
- Rzone的模式也就是单元化的模式,他更适用于用户侧的流水型的数据,比如评论、弹幕、动态、支付都是流水型的数据。
- Czone模式介于Gzone和Rzone,也是在用户间做数据共享,但它的可用区是可以做本地读写的,可以接受一定的数据延迟与不一致性。
B站有多个机房,但定位是有些混乱,在多活的场景里我们做了重新的梳理。将物理机房按照逻辑进行划分,映射到不同的可用区,以上海机房为例,四个机房分为两个可用区,这两个可用区就用来做Gzone的服务部署。因为两个可用区都在上海,做同城双活时网络延时特别低,一般情况下1毫秒左右,基本就不存在服务延时的问题了。 B站的很多业务场景都是适合做同城双活的,因为很多数据都是偏向于用户间共享的。下图右边是我们现在的同城双活模式架构图:由我们的DCDN层面也就是多活的Router层面来做我们的用户侧的请求路由分发(目前是基于用户的Mid或者用户的设备ID来做路由分发)分发到不同的可用区。不同可用区的服务会访问我们的缓存、KV存储和DB存储,这两个存储都会通过我们Proxy的模式去访问。中间还有一个GZS的组件,通过它来进行多活的全局管控。
在713故障之后,我们也对这些组件都做了一定优化。- DCDN层面:我们已经支持通过用户的ID或者用户设备ID来Hash路由到不同的机房,同时支持用户流量在多机房的动态权重,1/100到1/99,或者2/98都是可以动态支持的。
- Service层面:之前我们做同城双活时没有实现写,那个时候还没有Proxy,现在有了Proxy就可以让业务方来做本地的写改造了,然后通过存储的Proxy来路由到主可用区。
- Proxy层面:通过proxy同时支持KV和DB两种存储模式,并实现读写分离和failover。
- GZS层面:前面有讲到我们多活是没有全局管控的,那GZS就是来做我们的多活业务、业务应用和API的元信息管理的,包含业务、多活、切量的编排。
为了做好GZS我们内部研发了一个Invoker管控平台,它会与B站内部的业务树、Dashboard、Comet多个平台联动,从4个层面来优化我们之前的多活流程。基本的流程是编排—切量—审批—巡检—可观测,接下来挑选几个重点跟大家具体分享。1)多活定义编排
多活编排首先要确定这次多活的编排业务范围是一个业务还是一个业务域,评论、弹幕这些都是业务,他是边界比较明确的产品可以进行编排,而业务域是业务的集合,比如直播、电商这些就是业务域,它下面由哪些规则、哪些DB与KV的存储要全部编排下来工作量就会非常大,所以业务域是不支持编排的,我们采取的方式是业务先做好编排,再基于业务的元信息来聚合业务域。其次要确认业务它用了哪些DB,具体的多活模式是什么,Gzone还是Rzone。
到了接入层的编排:它使用的多活规则是什么,一个服务/产品在对外的时候一定有多个url以及规则,这里需要编排出哪些规则是支持多活的;这个多活规则在CDN层的调度它是使用用户的MID还是设备ID,还是基于其他方式来进行多活的流量分发;这些信息都是要明确编排的。B站目前消息层面的编排是暂不支持的,而数据层面的编排主要就是我们的KV和DB了,当这些编排都完成之后就会触发一个审批,由研发和SRE同学来确认这次的编排是否正确。2)多活切量编排
编排完成之后我们就可以发起切量了,切量时候你还是要先选择你的业务范围,是业务还是业务域。然后你要选择你切量的权重,每个可用区权重是多少,1:1甚至是1:99都是可以精确控制的。切量的编排还要明确这一次切量是否要切我们的存储,存储是否要切DB是否要切KV。大多数都是只切DCDN的流量层面就可以了,切DB的话可能会产生数据冲突和修数据的问题。
切量编排完成之后会触发切量审批,审批会过SRE以及CDN的负责人,同时通过工单的形式来明确一些操作,把每一个编排每一个层面的内容有哪些变更都会Diff出来,让大家直观地看到我们这次变更了哪些内容。3)切量可视化
上面的工作都完成之后就正式进入切量了,那第一步我们会进行一些风险预检,它主要分为3个部分:容量巡检:容量巡检的是我们应用的cpu,缓存的cpu、DB,包括KV的cpu等。在这里我们还会进行连接池的巡检,看看我们应用访问DB的连接池怎么样,应用访问缓存的连接池怎么样。之前我们在内部切量的时候踩过很多次坑,我们发现服务侧在不同的可用区是有不同的限流阈值的,切量后可用区流量翻倍导致限流阈值被触发影响到用户,所以我们现在还会做一些限流配置的巡检。延迟巡检:由于我们现在使用了Gzone的架构,所以DB和KV的同步延迟都比较低,像同城双活一般延迟就在一毫秒左右,这部分没有遇到什么问题。隔离巡检:隔离巡检主要是看我们的DB和KV的切换是否有跨业务的混用,比如说我要切评论的业务,结果我发现弹幕的业务也在引用这套DB,那我这个切量的影响范围就不止仅是评论了,那还包含弹幕,这就需要通知到弹幕相关的业务研发同学。可观测这一块,我们会监控前面巡检的相关指标、业务多活动流量、业务/应用SLO指标等内容。巡检相关指标在实际切量过程中也会发生变化,所以需要监控,而实时关注多活流量的状态是为了了解切量是不是符合预期。业务和应用SLO的指标的监控主要是应用的成功率、延迟这些应用侧核心指标的一个可视化。
切量平台本身我们也有做高和用,是按照同城双活的模式来部署的。针对切量平台本身的高可用工作分为三部分:第一部分是多活,切量平台按照业务架构的Gzone模式做了部署,平台所依赖的核心平台,比如DCDN、KV和DB平台也做了Gzone模式的部署。我们在进行切量演练的时候也会对Invoke平台的数据层来做切换的演练,比如切一下DB来验证平台否真正做到同城双活的容灾。第二部分是降级,713的时候因为登录不了鉴权系统导致我们不能及时处理问题,所以现在我们认证是可降级的,不强依赖我们的登录态。同样我们不能因为审批挂了,导致多活切不了,所以当审批平台挂了,审批是可以降级跳过的;同理假如说我预检不通过,是有风险的,但没有更好的应对措施,也是可以降级强制来做一个切量的。我们也会定期通过对工具平台做这些故障演练,模拟审批挂了或者是预检挂了来看我们平台的降级是否符合预期。第三部分是效率层面的,DCDN是肯定要切的,他是部署在全国各地的边缘节点,公网的稳定性一定是比不上内网的,所以针对这块场景我们也做了相关优化。比如说失败可以快速重试、部分节点失败可以先跳过,先保证可以切的节点全部切掉,以达到我们的预期,最后再来统一处理失败的节点。在切量的过程中如果发现服务有性能/容量问题,或者多机房承载不了流量导致不符合预期都可以做快速的一键回滚。
4、优化的效果:多活效率提升10倍
目前我们全部的多活业务都已经接入Invoke切量平台了,包括我们的主站、直播、OGV、电商等。我们在生产做了很多次演练,因为不在生产进行演练,当故障真正发生的时候,你是不敢切量的。H1期间我们就在生产大概切量演练了60+,过程没有出现过什么问题,也发现了很多持续优化的点,比如说巡检可观测。现在的切量流程是SRE跟研发确认我们切量的业务、组件、流量、执行,现在一个切量三到五分钟就完成了,而在此之前我们的切量都是半个小时起步的,效率大大提升。写在最后,故障其实并不可怕,我们要做的也不仅仅是修复故障,更重要的是要善于从踩过的坑中总结经验,找到优化和提升的方向,只有这样系统才能稳健发展。
后台回复关键词【7161】获取讲师PPT